/*
 * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
 *
 * This file is part of PortaPack.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street,
 * Boston, MA 02110-1301, USA.
 */

#include "portapack.hpp"
#include "portapack_hal.hpp"
#include "portapack_dma.hpp"
#include "portapack_cpld_data.hpp"
#include "portapack_persistent_memory.hpp"

#include "hackrf_hal.hpp"
#include "hackrf_gpio.hpp"
using namespace hackrf::one;

#include "clock_manager.hpp"

#include "backlight.hpp"
#include "touch_adc.hpp"
#include "audio.hpp"

#include "wm8731.hpp"
using wolfson::wm8731::WM8731;

#include "ak4951.hpp"
using asahi_kasei::ak4951::AK4951;

#include "cpld_update.hpp"

#include "optional.hpp"

namespace portapack {

portapack::IO io {
	portapack::gpio_dir,
	portapack::gpio_lcd_rdx,
	portapack::gpio_lcd_wrx,
	portapack::gpio_io_stbx,
	portapack::gpio_addr,
	portapack::gpio_lcd_te,
	portapack::gpio_unused,
};

portapack::BacklightCAT4004 backlight_cat4004;
portapack::BacklightOnOff   backlight_on_off;

lcd::ILI9341 display;

I2C i2c0(&I2CD0);
SPI ssp1(&SPID2);

si5351::Si5351 clock_generator {
	i2c0, hackrf::one::si5351_i2c_address
};

ClockManager clock_manager {
	i2c0, clock_generator
};

WM8731 audio_codec_wm8731 { i2c0, 0x1a };
AK4951 audio_codec_ak4951 { i2c0, 0x12 };

ReceiverModel receiver_model;

TemperatureLogger temperature_logger;

static constexpr uint32_t systick_count(const uint32_t clock_source_f) {
	return clock_source_f / CH_FREQUENCY;
}

static constexpr uint32_t systick_load(const uint32_t clock_source_f) {
	return systick_count(clock_source_f) - 1;
}

constexpr uint32_t i2c0_bus_f			= 400000;
constexpr uint32_t i2c0_high_period_ns	= 900;

typedef struct {
	uint32_t clock_f;
	uint32_t systick_count;
	uint32_t idivb;
	uint32_t idivc;
} clock_config_t;

static constexpr uint32_t idiv_config(const cgu::CLK_SEL clk_sel, const uint32_t idiv) {
	return cgu::IDIV_CTRL { 0, idiv-1, 1, clk_sel };
}

constexpr clock_config_t clock_config_irc {
	12000000, systick_load(12000000),
	idiv_config(cgu::CLK_SEL::IRC, 1),
	idiv_config(cgu::CLK_SEL::IRC, 1),
};

constexpr clock_config_t clock_config_pll1_boot {
	96000000, systick_load(96000000),
	idiv_config(cgu::CLK_SEL::PLL1, 9),
	idiv_config(cgu::CLK_SEL::PLL1, 3),
};

constexpr clock_config_t clock_config_pll1_step {
	100000000, systick_load(100000000),
	idiv_config(cgu::CLK_SEL::PLL1, 1),
	idiv_config(cgu::CLK_SEL::PLL1, 1),
};

constexpr clock_config_t clock_config_pll1 {
	200000000, systick_load(200000000),
	idiv_config(cgu::CLK_SEL::PLL1, 2),
	idiv_config(cgu::CLK_SEL::PLL1, 1),
};

constexpr I2CClockConfig i2c_clock_config_400k_boot_clock {
	.clock_source_f = clock_config_pll1_boot.clock_f,
	.bus_f = i2c0_bus_f,
	.high_period_ns = i2c0_high_period_ns,
};

constexpr I2CClockConfig i2c_clock_config_400k_fast_clock {
	.clock_source_f = clock_config_pll1.clock_f,
	.bus_f = i2c0_bus_f,
	.high_period_ns = i2c0_high_period_ns,
};

constexpr I2CConfig i2c_config_boot_clock {
	.high_count = i2c_clock_config_400k_boot_clock.i2c_high_count(),
	.low_count = i2c_clock_config_400k_boot_clock.i2c_low_count(),
};

constexpr I2CConfig i2c_config_fast_clock {
	.high_count = i2c_clock_config_400k_fast_clock.i2c_high_count(),
	.low_count = i2c_clock_config_400k_fast_clock.i2c_low_count(),
};

enum class PortaPackModel {
	R1_20150901,
	R2_20170522,
};

static PortaPackModel portapack_model() {
	static Optional<PortaPackModel> model;

	if( !model.is_valid() ) {
		if( audio_codec_wm8731.detected() ) {
			model = PortaPackModel::R1_20150901;
		} else {
			model = PortaPackModel::R2_20170522;
		}
	}

	return model.value();
}

static audio::Codec* portapack_audio_codec() {
	return (portapack_model() == PortaPackModel::R2_20170522)
		? static_cast<audio::Codec*>(&audio_codec_ak4951)
		: static_cast<audio::Codec*>(&audio_codec_wm8731)
		;
}

static const portapack::cpld::Config& portapack_cpld_config() {
	return (portapack_model() == PortaPackModel::R2_20170522)
		? portapack::cpld::rev_20170522::config
		: portapack::cpld::rev_20150901::config
		;
}

Backlight* backlight() {
	return (portapack_model() == PortaPackModel::R2_20170522)
		? static_cast<portapack::Backlight*>(&backlight_cat4004)
		: static_cast<portapack::Backlight*>(&backlight_on_off);
}

#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))

static LPC_CGU_BASE_CLK_Type* const base_clocks_idivc[] = {
	&LPC_CGU->BASE_PERIPH_CLK,
	&LPC_CGU->BASE_M4_CLK,
	&LPC_CGU->BASE_APB1_CLK,
	&LPC_CGU->BASE_APB3_CLK,
	&LPC_CGU->BASE_SDIO_CLK,
	&LPC_CGU->BASE_SSP1_CLK,
};

static void set_idivc_base_clocks(const cgu::CLK_SEL clock_source) {
	for(uint32_t i=0; i<ARRAY_SIZE(base_clocks_idivc); i++) {
		base_clocks_idivc[i]->AUTOBLOCK = 1;
		base_clocks_idivc[i]->CLK_SEL = toUType(clock_source);
	}
}

static void set_clock_config(const clock_config_t& config) {
	LPC_CGU->IDIVB_CTRL.word = config.idivb;
	LPC_CGU->IDIVC_CTRL.word = config.idivc;
	systick_adjust_period(config.systick_count);
	halLPCSetSystemClock(config.clock_f);
}

static void shutdown_base() {
	i2c0.stop();

	set_clock_config(clock_config_irc);

	cgu::pll1::disable();

	set_idivc_base_clocks(cgu::CLK_SEL::IRC);

	cgu::pll1::ctrl({
		.pd = 1,
		.bypass = 0,
		.fbsel = 0,
		.direct = 1,
		.psel = 0,
		.autoblock = 1,
		.nsel = 0,
		.msel = 23,
		.clk_sel = cgu::CLK_SEL::IRC,
	});

	cgu::pll1::enable();
	while( !cgu::pll1::is_locked() );

	set_clock_config(clock_config_pll1_boot);

	i2c0.start(i2c_config_boot_clock);

	clock_manager.shutdown();
}

/* Clock scheme after exiting bootloader in SPIFI mode:
 * 
 * XTAL_OSC = powered down
 *
 * PLL0USB = powered down
 * PLL0AUDIO = powered down
 * PLL1 = IRC * 24 = 288 MHz
 *
 * IDIVA = IRC / 1 = 12 MHz
 * IDIVB = PLL1 / 9 = 32 MHz
 * IDIVC = PLL1 / 3 = 96 MHz
 * IDIVD = IRC / 1 = 12 MHz
 * IDIVE = IRC / 1 = 12 MHz
 *
 * BASE_USB0_CLK = PLL0USB
 * BASE_PERIPH_CLK = IRC
 * BASE_M4_CLK = IDIVC (96 MHz)
 * BASE_SPIFI_CLK = IDIVB (32 MHZ)
 *
 * everything else = IRC
 */

/* Clock scheme during PortaPack operation:
 * 
 * XTAL_OSC = powered down
 *
 * PLL0USB = powered down
 * PLL0AUDIO = GP_CLKIN, Fcco=491.52 MHz, Fout=12.288 MHz
 * PLL1 =
 * 	OG: GP_CLKIN * 10 = 200 MHz
 *	r9: GP_CLKIN * 20 = 200 MHz
 * IDIVA = IRC / 1 = 12 MHz
 * IDIVB = PLL1 / 2 = 100 MHz
 * IDIVC = PLL1 / 1 = 200 MHz
 * IDIVD = PLL0AUDIO / N (where N is varied depending on decimation factor)
 * IDIVE = IRC / 1 = 12 MHz
 *
 * BASE_USB0_CLK = PLL0USB
 * BASE_PERIPH_CLK = IRC
 * BASE_M4_CLK = IDIVC (200 MHz)
 * BASE_SPIFI_CLK = IDIVB (100 MHZ)
 * BASE_AUDIO_CLK = IDIVD
 *
 * everything else = IRC
 */

bool init() {
	set_idivc_base_clocks(cgu::CLK_SEL::IDIVC);

	i2c0.start(i2c_config_boot_clock);

	if( !portapack::cpld::update_if_necessary(portapack_cpld_config()) ) {
		shutdown_base();
		return false;
	}

	if( !hackrf::cpld::load_sram() ) {
		chSysHalt();
	}

	configure_pins_portapack();
	
	portapack::io.init();

	clock_manager.init_clock_generator();

	i2c0.stop();

	set_clock_config(clock_config_irc);

	cgu::pll1::disable();

	/* Incantation from LPC43xx UM10503 section 12.2.1.1, to bring the M4
	 * core clock speed to the 110 - 204MHz range.
	 */

	/* Step into the 90-110MHz M4 clock range */
	/* OG:
	 * 	Fclkin = 40M
	 * 		/N=2 = 20M = PFDin
	 * 	Fcco = PFDin * (M=10) = 200M
	 * r9:
	 * 	Fclkin = 10M
	 * 		/N=1 = 10M = PFDin
	 * 	Fcco = PFDin * (M=20) = 200M
	 * Fclk = Fcco / (2*(P=1)) = 100M
	 */
	cgu::pll1::ctrl({
		.pd = 1,
		.bypass = 0,
		.fbsel = 0,
		.direct = 0,
		.psel = 0,
		.autoblock = 1,
		.nsel = hackrf_r9 ? 0UL : 1UL,
		.msel = hackrf_r9 ? 19UL : 9UL,
		.clk_sel = cgu::CLK_SEL::GP_CLKIN,
	});

	cgu::pll1::enable();
	while( !cgu::pll1::is_locked() );

	set_clock_config(clock_config_pll1_step);

	/* Delay >50us at 90-110MHz clock speed */
	volatile uint32_t delay = 1400;
	while(delay--);

	set_clock_config(clock_config_pll1);

	/* Remove /2P divider from PLL1 output to achieve full speed */
	cgu::pll1::direct();

	i2c0.start(i2c_config_fast_clock);

	clock_manager.set_reference_ppb(persistent_memory::correction_ppb());

	audio::init(portapack_audio_codec());
	
	clock_manager.enable_if_clocks();
	clock_manager.enable_codec_clocks();
	radio::init();

	touch::adc::init();

	LPC_CREG->DMAMUX = portapack::gpdma_mux;
	gpdma::controller.enable();

	return true;
}

void shutdown() {
	gpdma::controller.disable();

	backlight()->off();
	display.shutdown();
	
	radio::disable();
	audio::shutdown();

	hackrf::cpld::init_from_eeprom();

	shutdown_base();
}

} /* namespace portapack */
